﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Brain.Skinning;
using Microsoft.Xna.Framework;

namespace Brain
{
    public class SkinnedAnimationPlayer
    {
        /// <summary>
        /// The current reproducing Millisecond.
        /// </summary>
        public int CurrentMillisecond
        {
            get { return time; }
            protected set
            {
                time = value;
                UpdateAnimation();
            }
        }
        private int time = 0;

        public event SetBoneValues OnBoneCalculation;
        public event SetBoneMatrix OnBoneCalculated;
        public event SetSkinnedAnimationPlayer FinishedPlaying;

        /// <summary>
        /// The Skinned Model to Animate
        /// </summary>
        public SkinnedModel SkinnedModel { get; protected set; }

        /// <summary>
        /// The Crossfade time in Milliseconds. (Use 0 for no crossfade)
        /// </summary>
        public int CrossfadeTime = 300;

        public SkinnedAnimationPlayer(SkinnedModel model, SkinnedAnimationClip clip)
        {
            Playing = true;
            this.SkinnedModel = model;

            this.currentClip = clip;

            for (int i = 0; i < model.SkinTransforms.Length; i++)
            {
                model.SkinTransforms[i] = this.SkinnedModel.BindPose[i];
            }
        }

        public virtual void StartClip(SkinnedAnimationClip clip)
        {
            if (!isCrossfading)
            {
                this.currentClip = clip;
                this.currentFrame = 0;
                this.CurrentMillisecond = 0;
                Playing = true;
            }
        }

        /// <summary>
        /// Returns true if the crossFading was accepted(even if the CrossFadeTime is 0)
        /// </summary>
        /// <param name="clip"></param>
        /// <returns></returns>
        public virtual bool Crossfade(SkinnedAnimationClip clip)
        {
            if (!isCrossfading && clip != this.CurrentAnimationClip)
            {
                if (CrossfadeTime == 0)
                {
                    StartClip(clip);
                    return true;
                }
                else
                {
                    Playing = true;
                    this.crossfadingToClip = clip;
                    this.isCrossfading = true;
                    this.currentCrossfadeMS = 0;
                    return true;
                }
            }
            return false;
        }

        public SkinnedAnimationClip CurrentAnimationClip
        {
            get { return currentClip; }
        }

        private bool isCrossfading = false;
        private SkinnedAnimationClip crossfadingToClip;
        private float currentCrossfadeMS = 0;

        private SkinnedAnimationClip currentClip;
        private float currentFrame;

        public bool Playing { get; set; }

        public virtual void Update()
        {
            if (Engine.Instance.GameTime == null || !Playing)
            {
                return;
            }
            float elapsed = (float)Engine.Instance.GameTime.ElapsedGameTime.TotalMilliseconds;
            currentFrame += elapsed;

            if (isCrossfading)
            {
                if (currentCrossfadeMS > CrossfadeTime)
                {
                    isCrossfading = false;
                    currentClip = crossfadingToClip;
                    crossfadingToClip = null;
                    currentCrossfadeMS = 0;
                    currentFrame = 0;
                }
                else
                {
                    currentCrossfadeMS += elapsed;
                }
            }

            CurrentMillisecond = (int)currentFrame;
            if (currentFrame > currentClip.Duration)
            {
                if (FinishedPlaying != null)
                {
                    FinishedPlaying(this);
                }
                if (!Loop)
                {
                    Playing = false;
                }
                else
                {
                    while (currentFrame > currentClip.Duration)
                    {
                        currentFrame -= currentClip.Duration;
                    }
                }
            }
        }

        public bool Loop { get; set; }

        protected virtual void UpdateAnimation()
        {
            for (int i = 0; i < this.SkinnedModel.SkinTransforms.Length; i++)
            {
                UpdateBone(i);
            }

            for (int i = 0; i < this.SkinnedModel.SkinTransforms.Length; i++)
            {
                SkinnedBone bone = SkinnedModel.SkinnedBones[i];
                this.SkinnedModel.SkinTransforms[i] = bone.InverseBindPose * this.SkinnedModel.SkinTransforms[i];
            }
        }

        protected virtual void UpdateBone(int index)
        {
            if (currentClip.Bones.ContainsKey(index))
            {
                BoneKeyFrameCollection bone = currentClip.Bones[index];

                Matrix mat;
                if (isCrossfading)
                {
                    BoneKeyFrameCollection crossBone = this.crossfadingToClip.Bones[index];
                    CrossfadeBetweenFrames(bone, crossBone, out mat);
                }
                else
                {
                    InterpolateBetweenFrames(bone, out mat);
                }

                if (index != 0)
                {
                    mat *= this.SkinnedModel.SkinTransforms[bone.Bone.ParentBone];
                }
                this.SkinnedModel.SkinTransforms[index] = mat;
            }
            else
            {
                if (index == 0)
                {
                    this.SkinnedModel.SkinTransforms[index] = Matrix.Identity;
                }
                else
                {
                    SkinnedBone bone = this.SkinnedModel.SkinnedBones[index];
                    this.SkinnedModel.SkinTransforms[index] = this.SkinnedModel.BindPose[index] * this.SkinnedModel.SkinTransforms[bone.ParentBone];
                }
            }
        }

        protected virtual void InterpolateBetweenFrames(BoneKeyFrameCollection bone, out Matrix mat)
        {
            int frameIndex;
            Frame_Skinned frame = bone.FindKeyFrameByTime(CurrentMillisecond, out frameIndex);
            int nextIndex = (frameIndex + 1) % bone.KeyFrames.Length;
            Frame_Skinned frame2 = bone.KeyFrames[nextIndex];

            float timeBetween;
            if (frameIndex == (bone.KeyFrames.Length - 1))
                timeBetween = this.currentClip.Duration - frame.Time;
            else
                timeBetween = frame2.Time - frame.Time;

            Quaternion finalOri;
            Vector3 finalPos;
            Vector3 finalSca;
            if (timeBetween > 0)
            {
                int elapsedKeyframeTime = this.CurrentMillisecond - frame.Time;
                float amount = elapsedKeyframeTime / (float)timeBetween;

                finalOri = Quaternion.Lerp(frame.Orientation, frame2.Orientation, amount);
                finalPos = Vector3.Lerp(frame.Position, frame2.Position, amount);
                finalSca = Vector3.Lerp(frame.Scale, frame2.Scale, amount);
            }
            else
            {
                finalOri = frame.Orientation;
                finalPos = frame.Position;
                finalSca = frame.Scale;
            }

            if (OnBoneCalculation != null)
            {
                Vector3? pos = null;
                Vector3? sca = null;
                Quaternion? rot = null;

                OnBoneCalculation(bone,
                    finalSca, finalOri, finalSca,
                    ref sca, ref rot, ref pos);

                if (pos != null)
                {
                    finalPos = pos.Value;
                }
                if (sca != null)
                {
                    finalSca = sca.Value;
                }
                if (rot != null)
                {
                    finalOri = rot.Value;
                }
            }

            mat = Matrix.CreateFromQuaternion(finalOri);
            mat.Translation = finalPos;
            mat *= Matrix.CreateScale(finalSca);

            if (OnBoneCalculated != null)
            {
                Matrix? newMat = null;
                OnBoneCalculated(bone, mat, ref newMat);

                if (newMat != null)
                {
                    mat = newMat.Value;
                }
            }
        }
        protected virtual void CrossfadeBetweenFrames(BoneKeyFrameCollection bone, BoneKeyFrameCollection crossBone, out Matrix mat)
        {
            Frame_Skinned frame = bone.FindKeyFrameByTime(CurrentMillisecond);
            Frame_Skinned crossFrame = crossBone.KeyFrames[0];

            float amount = currentCrossfadeMS / (float)CrossfadeTime;

            Quaternion finalOri = Quaternion.Lerp(frame.Orientation, crossFrame.Orientation, amount);
            Vector3 finalPos = Vector3.Lerp(frame.Position, crossFrame.Position, amount);
            Vector3 finalSca = Vector3.Lerp(frame.Scale, crossFrame.Scale, amount);

            if (OnBoneCalculation != null)
            {
                Vector3? pos = null;
                Vector3? sca = null;
                Quaternion? rot = null;

                OnBoneCalculation(bone,
                    finalSca, finalOri, finalSca,
                    ref sca, ref rot, ref pos);

                if (pos != null)
                {
                    finalPos = pos.Value;
                }
                if (sca != null)
                {
                    finalSca = sca.Value;
                }
                if (rot != null)
                {
                    finalOri = rot.Value;
                }
            }

            mat = Matrix.CreateFromQuaternion(finalOri);
            mat.Translation = finalPos;
            mat *= Matrix.CreateScale(finalSca);

            if (OnBoneCalculated != null)
            {
                Matrix? newMat = null;
                OnBoneCalculated(bone, mat, ref newMat);

                if (newMat != null)
                {
                    mat = newMat.Value;
                }
            }
        }
    }
}
